iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
IT Operation

系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server系列 第 6

Day 6|PowerShell 錯誤處理:Script 出錯時,不要讓它直接停下來

  • 分享至 

  • xImage
  •  

Day 5 我們第一次讓 PowerShell 從「處理一台機器」走到「批次處理多台 Server」。

當時的流程大概是:

Server 清單

foreach

Test-Connection

判斷 Online / Offline

建立 Object

Export-Csv

如果環境非常單純,這支 Script 已經可以用了。

但實際做維運,很快就會遇到另一個問題:

如果執行到一半發生錯誤,怎麼辦?

例如今天要巡檢:

SERVER01
SERVER02
SERVER03
SERVER04
SERVER05

理想狀況當然是:

SERVER01 OK
SERVER02 OK
SERVER03 OK
SERVER04 OK
SERVER05 OK

但實際環境更可能是:

SERVER01 OK
SERVER02 DNS 找不到
SERVER03 OK
SERVER04 Access Denied
SERVER05 OK

如果 Script 遇到 SERVER02 就直接停止,那後面的 Server 都沒有檢查到。

所以今天要處理的是一個很重要的問題:

Script 發生錯誤時,怎麼記錄錯誤,但繼續完成其他工作?

可以執行,不代表可以拿來維運

剛開始學 PowerShell 時,我們通常會先關心:

Script 有沒有跑?
結果對不對?

但真正拿去做維運之後,問題會變成:

執行失敗時發生什麼?
哪一台 Server 失敗?
什麼時間失敗?
失敗原因是什麼?
其他 Server 有沒有繼續執行?

這些其實比單純「正常時可以跑」更重要。

因為系統工程師真正需要處理的,往往就是:

不正常的情況。

PowerShell 發生錯誤時會看到什麼?

先做一個簡單的測試。

Get-Item "C:\ThisFolderDoesNotExist"

如果這個路徑不存在,就會看到錯誤訊息。

例如:

Get-Item : Cannot find path
'C:\ThisFolderDoesNotExist'
because it does not exist.

PowerShell 已經告訴我們:

哪個 Command 出錯
發生什麼事情
哪個 Path 有問題

問題是,如果我們正在寫自動化 Script,不能只靠人在螢幕前看紅字。

我們需要讓程式自己知道:

「這裡出錯了。」

接著決定該怎麼處理。

今天最重要的四個東西

Day 6 主要會使用:

try

catch

finally

-ErrorAction

它們負責的事情可以先簡單理解成:

try

嘗試執行

成功

繼續

失敗

catch

記錄 / 處理錯誤

最後

finally
第一個 try / catch

先看最基本的寫法。

try {

Get-Item "C:\ThisFolderDoesNotExist"

}
catch {

Write-Host "發生錯誤"

}

看起來應該很合理。

但如果實際執行,有時候會發現:

catch 居然沒有執行。

這就要先理解 PowerShell 的兩種錯誤。

PowerShell 錯誤不完全一樣

PowerShell 的錯誤大致可以分成:

Terminating Error

Non-Terminating Error

可以先不用記英文。

簡單來說:

Terminating Error

這個錯誤嚴重到目前這個操作無法繼續。

這種錯誤通常可以直接進入:

catch
Non-Terminating Error

指令發生問題,但 PowerShell 覺得:

我還可以繼續執行後面的東西。

這種錯誤預設不一定會進入 catch。

而系統管理 Cmdlet 裡,這種情況其實很常見。

所以需要 -ErrorAction Stop

我們把剛才的指令改成:

try {

Get-Item `
    -Path "C:\ThisFolderDoesNotExist" `
    -ErrorAction Stop

}
catch {

Write-Host "發生錯誤"

}

這次如果 Path 不存在,就會進到:

catch

這裡的關鍵就是:

-ErrorAction Stop

它的意思可以理解成:

如果這個 Cmdlet 發生錯誤,把它當成需要立即處理的錯誤。

這樣 try / catch 才可以接手。

catch 裡面怎麼知道發生什麼錯?

如果只寫:

catch {

Write-Host "發生錯誤"

}

其實幫助不大。

因為真的出問題時,我們最想知道的是:

到底錯在哪裡?

PowerShell 在 catch 裡可以使用:

$_

沒錯,又看到 $_ 了。

Day 3 裡:

$_

代表 Pipeline 正在處理的 Object。

在:

catch

裡面,$_ 則代表:

目前捕捉到的 Error Record。

例如:

try {

Get-Item `
    -Path "C:\ThisFolderDoesNotExist" `
    -ErrorAction Stop

}
catch {

Write-Host "Error:"
Write-Host $_

}

就能看到真正的錯誤訊息。

只取得錯誤訊息

如果不想把整個 Error Record 全部輸出,可以:

$_.Exception.Message

例如:

try {

Get-Item `
    -Path "C:\ThisFolderDoesNotExist" `
    -ErrorAction Stop

}
catch {

Write-Host "Error Message:"
Write-Host $_.Exception.Message

}

可能得到:

Error Message:
Cannot find path 'C:\ThisFolderDoesNotExist'
because it does not exist.

這個就很適合寫進 Log。

把錯誤處理放進 Server 巡檢

回到 Day 5。

我們原本有:

foreach ($Server in $Servers) {

$PingResult = Test-Connection `
    -ComputerName $Server `
    -Count 1 `
    -Quiet

}

現在改成:

foreach ($Server in $Servers) {

try {

    $PingResult = Test-Connection `
        -ComputerName $Server `
        -Count 1 `
        -Quiet `
        -ErrorAction Stop

}
catch {

    Write-Host "$Server 發生錯誤"
    Write-Host $_.Exception.Message

}

}

這樣其中一台 Server 發生錯誤時,我們至少可以知道原因。

而不需要讓整支 Script 直接失去控制。

Offline 跟 Error 其實不是同一件事

這裡開始要把狀態分得更清楚。

假設:

Test-Connection

沒有收到 ICMP Response。

可能代表:

Online
Offline
Error

三種狀況最好不要混在一起。

例如:

SERVER01 → Online
SERVER02 → Offline
ABC123 → DNS / Name Resolution Error

SERVER02 可能真的存在,只是沒有回 Ping。

但 ABC123 可能連 Hostname 都不存在。

對維運來說,這兩件事情的排查方向完全不同。

把狀態分成 Online、Offline、Error

我們可以改成:

foreach ($Server in $Servers) {

try {

    $PingResult = Test-Connection `
        -ComputerName $Server `
        -Count 1 `
        -Quiet `
        -ErrorAction Stop

    if ($PingResult) {

        $Status = "Online"
        $ErrorMessage = ""

    }
    else {

        $Status = "Offline"
        $ErrorMessage = ""

    }

}
catch {

    $Status = "Error"
    $ErrorMessage = $_.Exception.Message

}

}

現在就開始有三種結果:

Online
Offline
Error

這比單純:

True
False

更適合實際維運。

把錯誤也放進 PSCustomObject

Day 5 我們使用:

[PSCustomObject]

建立自己的資料。

今天再加入:

ErrorMessage

完整一點:

$Result = [PSCustomObject]@{
Server = $Server
Status = $Status
CheckTime = Get-Date
ErrorMessage = $ErrorMessage
}

結果可能變成:

Server Status CheckTime ErrorMessage
SERVER01 Online 2026/09/14 05:30
SERVER02 Offline 2026/09/14 05:30
WRONGSRV Error 2026/09/14 05:30 No such host is known

現在報表價值就比昨天高很多。

因為不只是說:

失敗。

而是留下:

為什麼失敗。

第一版完整錯誤處理

先組合看看:

$Servers = @(
"SERVER01"
"SERVER02"
"WRONG-SERVER-NAME"
)

$Results = @()

foreach ($Server in $Servers) {

try {

    Write-Host "Checking $Server..."

    $PingResult = Test-Connection `
        -ComputerName $Server `
        -Count 1 `
        -Quiet `
        -ErrorAction Stop

    if ($PingResult) {

        $Status = "Online"
        $ErrorMessage = ""

    }
    else {

        $Status = "Offline"
        $ErrorMessage = ""

    }

}
catch {

    $Status = "Error"
    $ErrorMessage = $_.Exception.Message

}

$Result = [PSCustomObject]@{
    Server       = $Server
    Status       = $Status
    CheckTime    = Get-Date
    ErrorMessage = $ErrorMessage
}

$Results += $Result

}

最後:

$Results

可能得到:

Server Status CheckTime ErrorMessage


SERVER01 Online 2026/09/14 05:32
SERVER02 Offline 2026/09/14 05:32
WRONG-SERVER-NAME Error 2026/09/14 05:32 No such host...

這就已經比 Day 5 的 Script 健壯很多了。

finally 是做什麼的?

除了:

try
catch

還有:

finally

它比較特別。

不管:

try 成功

還是:

try 失敗 → catch

finally 都會執行。

例如:

try {

Write-Host "開始執行"

Get-Item `
    "C:\Test" `
    -ErrorAction Stop

}
catch {

Write-Host "發生錯誤"

}
finally {

Write-Host "這段一定會執行"

}

不管 C:\Test 存不存在:

這段一定會執行

都會出現。

finally 什麼時候有用?

例如 Script 開始時建立了一個:

Connection
Session
Temporary File

不管成功或失敗,最後都要:

關閉 Connection
移除 Session
刪除 Temporary File

這種事情就很適合放在:

finally

今天的 Ping Script 其實還沒有非常需要 finally。

所以我不會為了使用它硬塞進去。

但先知道:

需要「不管成功還是失敗都要執行」的清理工作時,可以考慮 finally。

Script 發生錯誤,最好還要有 Log

現在 CSV 已經會留下結果。

但一般維運工具我還會希望有一個:

Log

因為報表通常是給人看的結果。

Log 比較偏:

程式執行過程發生了什麼?

例如:

2026-09-14 05:35:01 [INFO] Starting Server Check
2026-09-14 05:35:02 [INFO] Checking SERVER01
2026-09-14 05:35:02 [OK] SERVER01 Online
2026-09-14 05:35:03 [INFO] Checking SERVER02
2026-09-14 05:35:06 [WARN] SERVER02 Offline
2026-09-14 05:35:07 [ERROR] WRONGSRV - No such host is known
2026-09-14 05:35:07 [INFO] Check Completed

這對排查自己的 Automation Script 非常有幫助。

PowerShell 怎麼寫 Log?

最簡單可以用:

Add-Content

例如:

Add-Content -Path "C:\Temp\ServerCheck.log"
-Value "Server Check Started"

執行後:

C:\Temp\ServerCheck.log

就會多一行:

Server Check Started

但正式一點,我們通常還要加入時間。

加入 Timestamp

例如:

$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

然後:

Add-Content -Path "C:\Temp\ServerCheck.log"
-Value "$Time [INFO] Server Check Started"

結果:

2026-09-14 05:40:30 [INFO] Server Check Started

這已經開始有 Log 的樣子了。

不要每一次都重複寫 Add-Content

如果程式裡到處都是:

$Time = Get-Date
Add-Content ...

很快就會很亂。

所以這裡第一次可以稍微接觸:

function

我們建立:

function Write-Log {

param (
    $Message,
    $Level = "INFO"
)

$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

Add-Content `
    -Path $LogFile `
    -Value "$Time [$Level] $Message"

}

之後只需要:

Write-Log "Server Check Started"

或:

Write-Log "SERVER01 is Online" "OK"

甚至:

Write-Log "Cannot connect SERVER02" "ERROR"

Log 就會統一格式。

Function 可以先怎麼理解?

今天不用深入 Function。

先把它理解成:

把一段會重複使用的程式包起來,取一個名字。

原本一直寫:

$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

Add-Content -Path $LogFile
-Value "$Time [INFO] Something"

現在只要:

Write-Log "Something"

後面我們還會再正式使用 Function。

今天只是因為 Log 非常適合拿來示範。

今天完整版本:Server Check + Error Handling + Log

把 Day 4、Day 5、Day 6 組起來:

==========================================

Multi Server Connectivity Check

With Error Handling and Logging

==========================================

$Servers = @(
"SERVER01"
"SERVER02"
"WRONG-SERVER-NAME"
)

$ReportFolder = "C:\Temp"
$Date = Get-Date -Format "yyyyMMdd"

$ReportFile = "$ReportFolder\Server_Status_$Date.csv"
$LogFile = "$ReportFolder\Server_Check_$Date.log"

--------------------------

Create Report Folder

--------------------------

if (-not (Test-Path $ReportFolder)) {

New-Item `
    -Path $ReportFolder `
    -ItemType Directory | Out-Null

}

--------------------------

Log Function

--------------------------

function Write-Log {

param (
    $Message,
    $Level = "INFO"
)

$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

Add-Content `
    -Path $LogFile `
    -Value "$Time [$Level] $Message"

}

--------------------------

Start

--------------------------

Write-Log "Server connectivity check started."

$Results = @()

foreach ($Server in $Servers) {

Write-Host "Checking $Server..."

Write-Log "Checking $Server"

try {

    $PingResult = Test-Connection `
        -ComputerName $Server `
        -Count 1 `
        -Quiet `
        -ErrorAction Stop

    if ($PingResult) {

        $Status = "Online"
        $ErrorMessage = ""

        Write-Log "$Server is Online" "OK"

    }
    else {

        $Status = "Offline"
        $ErrorMessage = ""

        Write-Log "$Server did not respond to ICMP" "WARN"

    }

}
catch {

    $Status = "Error"

    $ErrorMessage = $_.Exception.Message

    Write-Log `
        "$Server - $ErrorMessage" `
        "ERROR"

}


$Result = [PSCustomObject]@{

    Server       = $Server
    Status       = $Status
    CheckTime    = Get-Date
    ErrorMessage = $ErrorMessage

}

$Results += $Result

}

--------------------------

Export Report

--------------------------

$Results |
Export-Csv -Path $ReportFile
-NoTypeInformation `
-Encoding UTF8

Write-Log "Server connectivity check completed."

Write-Host ""
Write-Host "Check completed."
Write-Host "Report : $ReportFile"
Write-Host "Log : $LogFile"
執行後現在會產生兩個檔案

以前 Day 5 只有:

Server_Status_20260914.csv

現在變成:

C:\Temp

├── Server_Status_20260914.csv

└── Server_Check_20260914.log

CSV 負責:

最終結果

Log 負責:

執行過程

這兩個角色最好分開。

CSV 可能長這樣
Server,Status,CheckTime,ErrorMessage

SERVER01,Online,2026/09/14 05:50:01,
SERVER02,Offline,2026/09/14 05:50:05,
WRONG-SERVER-NAME,Error,2026/09/14 05:50:06,No such host is known

而 Log:

2026-09-14 05:50:00 [INFO] Server connectivity check started.
2026-09-14 05:50:01 [INFO] Checking SERVER01
2026-09-14 05:50:01 [OK] SERVER01 is Online
2026-09-14 05:50:02 [INFO] Checking SERVER02
2026-09-14 05:50:05 [WARN] SERVER02 did not respond to ICMP
2026-09-14 05:50:05 [INFO] Checking WRONG-SERVER-NAME
2026-09-14 05:50:06 [ERROR] WRONG-SERVER-NAME - No such host is known
2026-09-14 05:50:06 [INFO] Server connectivity check completed.

如果三天後有人問:

為什麼這次巡檢少了一台?

我們至少有東西可以往回查。

另一個很好用的東西:$Error

PowerShell 本身也會保存最近發生過的錯誤。

可以輸入:

$Error

查看 Error History。

最新的一筆:

$Error[0]

例如:

$Error[0].Exception.Message

就可以看最新錯誤的 Message。

這在 Debug Script 時很好用。

不過正式 Automation 還是不建議完全依賴:

$Error

因為它比較像目前 PowerShell Session 裡的錯誤歷史。

真正需要長期追蹤,還是應該自己寫:

Log
-ErrorAction 還有哪些模式?

今天主要使用:

-ErrorAction Stop

另外常見的還有:

Continue
SilentlyContinue
Stop

例如:

-ErrorAction SilentlyContinue

意思比較接近:

發生錯誤就不要顯示錯誤訊息,繼續執行。

有時候很好用。

例如:

Get-Process "Notepad" `
-ErrorAction SilentlyContinue

如果 Notepad 不存在,就不會一直跳紅字。

但這個選項也要小心。

因為:

看不到 Error,不代表沒有 Error。

如果 Script 很重要,我反而會比較偏向:

Error 發生

Catch

記錄原因

而不是直接全部:

SilentlyContinue

把錯誤藏起來。

不要用 try / catch 把所有問題吃掉

這也是寫 Automation 很容易發生的另一個問題。

有人可能會寫:

try {

# 一大堆程式

}
catch {

}

catch 裡什麼都不做。

這樣畫面確實很乾淨。

但是如果 Script 執行失敗,我們也完全不知道原因。

這其實比看到紅字更麻煩。

比較好的做法至少留下:

Time
Server
Action
ErrorMessage

這樣未來才有辦法追。

維運 Script 最怕「看起來執行成功」

我覺得這是今天最重要的一件事情。

假設 Script 最後顯示:

Completed.

但中間其實:

50 台 Server
成功 20 台
失敗 30 台

如果沒有 Log、沒有 Status、沒有 Error Message,

那這個:

Completed

其實沒有太大意義。

所以以後看到:

Script 執行完畢

我們最好再問:

是程式跑到最後了,還是工作真的全部成功?

這兩件事情是不一樣的。

我會怎麼定義一支比較能用的維運 Script?

目前我們的 Script 已經開始有這幾層:

Input

Server List

Processing

foreach

Action

Test-Connection

Decision

if

Error Handling

try / catch

Result

PSCustomObject

Report

CSV

Trace

Log

跟 Day 2 的:

Get-Service

相比,已經開始完全不一樣了。

但本質上還是把前面每天學的一點東西慢慢加進來。

Day 6 小結

今天我們沒有增加很多新的系統管理功能。

但我們把 Script 很重要的一塊補起來了:

錯誤處理。

最需要記住的基本結構就是:

try {

# 可能發生錯誤的工作

}
catch {

# 錯誤發生後怎麼處理

}
finally {

# 不管成功失敗都要執行的工作

}

搭配:

-ErrorAction Stop

以及:

$_.Exception.Message

把真正的錯誤原因留下來。

我們的 Server Check 也從:

Online / Offline

進一步變成:

Online
Offline
Error
+
Error Message
+
CSV Report
+
Log

這樣才開始比較接近真正可以拿來長期使用的維運工具。


上一篇
Day 5|PowerShell 變數、陣列與迴圈:一次處理多台 Server 的第一步
下一篇
Day 7|Windows Server 健康檢查:CPU、Memory、Disk 一次抓出來
系列文
系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言